${data.businessName} ${data.phone}

${serviceType.name} in ${data.location}

${serviceType.heroText}. Fast response, competitive prices, and workmanship you can trust.

Our Services

We provide a comprehensive range of ${serviceType.name.toLowerCase()} across ${data.location} and surrounding areas.

${data.services.map(service => `
${serviceType.icon}

${service}

Professional ${service.toLowerCase()} services in ${data.location}. Fast response and competitive rates.

Learn more →
`).join('')}

Why Choose ${data.businessName}?

Fast Response

Quick response times for all enquiries and emergencies

💷

Fair Pricing

Competitive rates with no hidden charges

Fully Qualified

Experienced, professional technicians

🏆

Guaranteed Work

All work completed to the highest standards

Areas We Cover

Providing ${serviceType.name.toLowerCase()} across ${data.location} and surrounding areas

${data.areas.map(area => `${area}`).join('')}

Need ${serviceType.name}?

Get in touch today for a free, no-obligation quote

Call ${data.phone}
`; } function generateServicesPage(data) { const serviceType = SERVICE_TYPES[data.serviceType]; return ` Our Services | ${data.businessName} - ${serviceType.name} in ${data.location}
${data.businessName} ${data.phone}

Our Services

Professional ${serviceType.name.toLowerCase()} for homes and businesses in ${data.location}

${data.services.map((service, i) => `
${serviceType.icon}

${service}

Our professional ${service.toLowerCase()} service covers all aspects of ${service.toLowerCase()} for residential and commercial properties in ${data.location}. We use the latest equipment and techniques to ensure the best results.

  • Fast response times
  • Competitive pricing
  • Fully qualified technicians
  • Guaranteed workmanship
Get a Quote
`).join('')}

Ready to Get Started?

Contact us today for a free quote

Call ${data.phone}
`; } function generateAreasPage(data) { const serviceType = SERVICE_TYPES[data.serviceType]; return ` Areas We Cover | ${data.businessName} - ${serviceType.name} in ${data.location}
${data.businessName} ${data.phone}

Areas We Cover

Providing ${serviceType.name.toLowerCase()} across ${data.location} and beyond

${data.areas.map(area => `

${serviceType.name} in ${area}

Professional ${serviceType.name.toLowerCase()} covering ${area} and surrounding areas. Fast response and competitive rates.

Services available:

${data.services.slice(0, 3).join(' • ')}

`).join('')}

Covering ${data.location} & Surrounding Areas

Can't see your area? Give us a call - we may still be able to help!

${data.phone}
`; } function generateContactPage(data) { const serviceType = SERVICE_TYPES[data.serviceType]; return ` Contact Us | ${data.businessName} - ${serviceType.name} in ${data.location}
${data.businessName} ${data.phone}

Contact Us

Get in touch for a free, no-obligation quote

Get In Touch

📞

Phone

${data.phone}

Available 7 days a week

✉️

Email

${data.email}

We'll respond within 24 hours

📍

Service Area

${data.location}

And surrounding areas

Why Choose Us?

  • Free, no-obligation quotes
  • Fast response times
  • Fully qualified technicians
  • Competitive pricing

Request a Quote

We respect your privacy. Your information will never be shared.

`; } // Generate individual service pages function generateServicePage(data, service) { const serviceType = SERVICE_TYPES[data.serviceType]; const slug = service.toLowerCase().replace(/\s+/g, '-'); return ` ${service} in ${data.location} | ${data.businessName}
${data.businessName} ${data.phone}

${service} in ${data.location}

Professional ${service.toLowerCase()} services for homes and businesses. Fast response, competitive prices, and guaranteed workmanship.

Call Now: ${data.phone}

Expert ${service} Services

At ${data.businessName}, we provide professional ${service.toLowerCase()} services throughout ${data.location} and the surrounding areas. Our experienced team is equipped with the latest tools and technology to handle any ${service.toLowerCase()} requirement, big or small.

Whether you're a homeowner dealing with an unexpected issue or a business requiring regular maintenance, we're here to help with fast, reliable service at competitive prices.

Why Choose Our ${service} Service?

  • Fast Response: We understand that ${service.toLowerCase()} issues can be urgent. That's why we offer rapid response times across ${data.location}.
  • Experienced Team: Our technicians are fully trained and experienced in all aspects of ${service.toLowerCase()}.
  • Competitive Pricing: We offer transparent, competitive pricing with no hidden charges.
  • Guaranteed Work: All our work is completed to the highest standards and fully guaranteed.

Areas We Cover for ${service}

We provide ${service.toLowerCase()} services across ${data.location} and surrounding areas including:

${data.areas.map(area => `${area}`).join('')}

Get a Free Quote

Need ${service.toLowerCase()}? Contact us today for a free, no-obligation quote.

${data.phone} Request Callback

Other Services

    ${data.services.filter(s => s !== service).slice(0, 4).map(s => `
  • ${s}
  • ` ).join('')}

Need ${service}?

Get in touch today for fast, professional service

${data.phone}
`; } // Generate area page function generateAreaPage(data, area) { const serviceType = SERVICE_TYPES[data.serviceType]; return ` ${serviceType.name} in ${area} | ${data.businessName}
${data.businessName} ${data.phone}

${serviceType.name} in ${area}

Your local ${serviceType.name.toLowerCase()} experts serving ${area} and surrounding areas. Fast response, competitive prices.

Call Now: ${data.phone}

Local ${serviceType.name} Experts in ${area}

${data.businessName} provides professional ${serviceType.name.toLowerCase()} throughout ${area}. As local experts, we understand the specific needs of properties in the ${area} area and deliver fast, reliable service every time.

Our team of experienced professionals is available 7 days a week to handle all your ${serviceType.name.toLowerCase()} requirements, from emergency call-outs to planned maintenance work.

Services Available in ${area}

${data.services.map(service => `

${service}

Professional ${service.toLowerCase()} in ${area}

`).join('')}

Why ${area} Residents Choose Us

  • Local Knowledge: We know ${area} well, ensuring fast arrival times and understanding of local requirements.
  • Trusted Reputation: We've built a strong reputation for quality work in ${area} and surrounding areas.
  • Emergency Service: Available for emergency call-outs in ${area} when you need us most.

Get a Free Quote in ${area}

Need ${serviceType.name.toLowerCase()} in ${area}? Contact us today.

${data.phone} Request Callback

Nearby Areas

    ${data.areas.filter(a => a !== area).slice(0, 5).map(a => `
  • ${a}
  • ` ).join('')}

Need ${serviceType.name} in ${area}?

Call now for fast, professional service

${data.phone}
`; } // Main App Component function App() { const [step, setStep] = useState(1); const [generating, setGenerating] = useState(false); const [progress, setProgress] = useState(0); const [formData, setFormData] = useState({ businessName: '', serviceType: 'drainage', location: '', phone: '', email: '', services: [], areas: [] }); const [customService, setCustomService] = useState(''); const [customArea, setCustomArea] = useState(''); const [postcode, setPostcode] = useState(''); const [lookingUp, setLookingUp] = useState(false); const [lookupError, setLookupError] = useState(''); // Lookup postcode and get nearby areas const lookupPostcode = async () => { const pc = postcode.trim().replace(/\s+/g, ''); if (!pc) { setLookupError('Please enter a postcode'); return; } setLookingUp(true); setLookupError(''); try { const mainRes = await fetch(`https://api.postcodes.io/postcodes/${pc}`); const mainData = await mainRes.json(); if (!mainData.result) { setLookupError('Postcode not found. Try another.'); setLookingUp(false); return; } const mainArea = mainData.result; const mainTown = mainArea.admin_ward || mainArea.parish || mainArea.admin_district; updateForm('location', mainArea.admin_district || mainTown); const nearbyRes = await fetch(`https://api.postcodes.io/postcodes/${pc}/nearest?limit=30`); const nearbyData = await nearbyRes.json(); const areaSet = new Set(); if (mainArea.admin_ward) areaSet.add(mainArea.admin_ward); if (mainArea.parish && mainArea.parish !== mainArea.admin_ward) areaSet.add(mainArea.parish); if (nearbyData.result) { nearbyData.result.forEach(p => { if (p.admin_ward) areaSet.add(p.admin_ward); if (p.parish && p.parish !== p.admin_ward) areaSet.add(p.parish); }); } // Try outcode for broader coverage const outcode = pc.slice(0, -3).trim() || pc.slice(0, 3); try { const outcodeRes = await fetch(`https://api.postcodes.io/outcodes/${outcode}/nearest?limit=10`); const outcodeData = await outcodeRes.json(); if (outcodeData.result) { outcodeData.result.forEach(oc => { if (oc.admin_district) oc.admin_district.forEach(d => areaSet.add(d)); if (oc.admin_ward) oc.admin_ward.forEach(w => areaSet.add(w)); }); } } catch (e) {} const areas = Array.from(areaSet) .filter(a => a && a.length > 2) .sort((a, b) => a.localeCompare(b)) .slice(0, 20); updateForm('areas', areas); setLookingUp(false); } catch (err) { setLookupError('Lookup failed. Check the postcode and try again.'); setLookingUp(false); } }; const updateForm = (field, value) => { setFormData(prev => ({ ...prev, [field]: value })); }; const addService = (service) => { if (service && !formData.services.includes(service)) { updateForm('services', [...formData.services, service]); } setCustomService(''); }; const removeService = (service) => { updateForm('services', formData.services.filter(s => s !== service)); }; const addArea = (area) => { if (area && !formData.areas.includes(area)) { updateForm('areas', [...formData.areas, area]); } setCustomArea(''); }; const removeArea = (area) => { updateForm('areas', formData.areas.filter(a => a !== area)); }; const loadDefaultServices = () => { const defaults = SERVICE_TYPES[formData.serviceType].services; updateForm('services', [...new Set([...formData.services, ...defaults])]); }; const generateSite = async () => { setGenerating(true); setProgress(0); const zip = new JSZip(); const totalFiles = 4 + formData.services.length + formData.areas.length; let completed = 0; const updateProgress = () => { completed++; setProgress(Math.round((completed / totalFiles) * 100)); }; // Generate main pages await new Promise(r => setTimeout(r, 100)); zip.file('index.html', generateHomePage(formData)); updateProgress(); await new Promise(r => setTimeout(r, 100)); zip.file('services.html', generateServicesPage(formData)); updateProgress(); await new Promise(r => setTimeout(r, 100)); zip.file('areas.html', generateAreasPage(formData)); updateProgress(); await new Promise(r => setTimeout(r, 100)); zip.file('contact.html', generateContactPage(formData)); updateProgress(); // Generate individual service pages for (const service of formData.services) { await new Promise(r => setTimeout(r, 50)); const slug = service.toLowerCase().replace(/\s+/g, '-'); zip.file(`${slug}.html`, generateServicePage(formData, service)); updateProgress(); } // Generate area pages for (const area of formData.areas) { await new Promise(r => setTimeout(r, 50)); const slug = area.toLowerCase().replace(/\s+/g, '-'); zip.file(`${slug}.html`, generateAreaPage(formData, area)); updateProgress(); } // Generate ZIP and download const blob = await zip.generateAsync({ type: 'blob' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${formData.businessName.toLowerCase().replace(/\s+/g, '-')}-website.zip`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); setGenerating(false); setStep(4); }; const canProceed = () => { if (step === 1) return formData.businessName && formData.location && formData.phone && formData.email; if (step === 2) return formData.services.length >= 3; if (step === 3) return formData.areas.length >= 3; return true; }; return h('div', { className: 'min-h-screen' }, // Hero gradient h('div', { className: 'hero-gradient absolute inset-0 pointer-events-none' }), // Header h('header', { className: 'relative border-b border-white/10' }, h('div', { className: 'max-w-5xl mx-auto px-6 py-6 flex justify-between items-center' }, h('div', { className: 'brand text-2xl font-bold text-white' }, 'LocalSite', h('span', { className: 'text-emerald-400' }, 'Pro')), h('div', { className: 'text-sm text-gray-400' }, 'Generate Professional Service Websites') ) ), // Main content h('main', { className: 'relative max-w-3xl mx-auto px-6 py-12' }, // Step indicators h('div', { className: 'flex justify-center gap-4 mb-12' }, [1, 2, 3].map(s => h('div', { key: s, className: `step-indicator w-10 h-10 rounded-full flex items-center justify-center font-bold ${step > s ? 'complete text-white' : step === s ? 'active text-emerald-400' : 'text-gray-500'}` }, step > s ? '✓' : s) ) ), // Step 1: Business Details step === 1 && h('div', { className: 'card rounded-2xl p-8 glow-border fade-in' }, h('h2', { className: 'text-2xl font-bold text-white mb-2' }, 'Business Details'), h('p', { className: 'text-gray-400 mb-8' }, 'Tell us about your business'), h('div', { className: 'space-y-6' }, h('div', null, h('label', { className: 'block text-sm font-medium text-gray-300 mb-2' }, 'Business Name'), h('input', { type: 'text', value: formData.businessName, onChange: e => updateForm('businessName', e.target.value), className: 'input-field w-full px-4 py-3 rounded-lg text-white', placeholder: 'e.g. Quick Drains Swindon' }) ), h('div', null, h('label', { className: 'block text-sm font-medium text-gray-300 mb-2' }, 'Service Type'), h('select', { value: formData.serviceType, onChange: e => updateForm('serviceType', e.target.value), className: 'input-field w-full px-4 py-3 rounded-lg text-white' }, Object.entries(SERVICE_TYPES).map(([key, val]) => h('option', { key, value: key }, `${val.icon} ${val.name}`) ) ) ), // Postcode lookup section h('div', { className: 'bg-emerald-500/10 border border-emerald-500/30 rounded-xl p-4' }, h('label', { className: 'block text-sm font-medium text-emerald-400 mb-1' }, '🔍 Auto-fill Location & Areas'), h('p', { className: 'text-xs text-gray-400 mb-3' }, 'Enter a UK postcode to automatically find your town and nearby areas'), h('div', { className: 'flex gap-2' }, h('input', { type: 'text', value: postcode, onChange: e => setPostcode(e.target.value.toUpperCase()), onKeyPress: e => e.key === 'Enter' && lookupPostcode(), className: 'input-field flex-1 px-4 py-3 rounded-lg text-white uppercase', placeholder: 'e.g. SN1 1AA' }), h('button', { onClick: lookupPostcode, disabled: lookingUp, className: 'px-6 py-3 bg-emerald-500 text-white rounded-lg font-medium hover:bg-emerald-600 disabled:opacity-50 transition-all whitespace-nowrap' }, lookingUp ? 'Looking up...' : 'Find Areas') ), lookupError && h('p', { className: 'text-red-400 text-xs mt-2' }, lookupError), formData.areas.length > 0 && h('p', { className: 'text-emerald-400 text-xs mt-2' }, `✓ Found ${formData.areas.length} nearby areas`) ), h('div', null, h('label', { className: 'block text-sm font-medium text-gray-300 mb-2' }, 'Main Location / Town'), h('input', { type: 'text', value: formData.location, onChange: e => updateForm('location', e.target.value), className: 'input-field w-full px-4 py-3 rounded-lg text-white', placeholder: 'e.g. Swindon (or auto-filled from postcode)' }) ), h('div', { className: 'grid md:grid-cols-2 gap-4' }, h('div', null, h('label', { className: 'block text-sm font-medium text-gray-300 mb-2' }, 'Phone Number'), h('input', { type: 'tel', value: formData.phone, onChange: e => updateForm('phone', e.target.value), className: 'input-field w-full px-4 py-3 rounded-lg text-white', placeholder: 'e.g. 01234 567890' }) ), h('div', null, h('label', { className: 'block text-sm font-medium text-gray-300 mb-2' }, 'Email Address'), h('input', { type: 'email', value: formData.email, onChange: e => updateForm('email', e.target.value), className: 'input-field w-full px-4 py-3 rounded-lg text-white', placeholder: 'e.g. info@example.com' }) ) ) ), h('button', { onClick: () => setStep(2), disabled: !canProceed(), className: 'btn-primary w-full mt-8 py-4 rounded-xl font-bold text-white text-lg' }, 'Continue to Services →') ), // Step 2: Services step === 2 && h('div', { className: 'card rounded-2xl p-8 glow-border fade-in' }, h('h2', { className: 'text-2xl font-bold text-white mb-2' }, 'Services Offered'), h('p', { className: 'text-gray-400 mb-6' }, 'Select or add the services you offer (minimum 3)'), h('button', { onClick: loadDefaultServices, className: 'mb-6 px-4 py-2 bg-emerald-500/20 text-emerald-400 rounded-lg text-sm font-medium hover:bg-emerald-500/30 transition-all' }, `+ Load default ${SERVICE_TYPES[formData.serviceType].name} services`), // Selected services formData.services.length > 0 && h('div', { className: 'flex flex-wrap gap-2 mb-6' }, formData.services.map(service => h('span', { key: service, className: 'tag px-3 py-2 rounded-lg text-white flex items-center gap-2' }, service, h('button', { onClick: () => removeService(service), className: 'text-gray-400 hover:text-red-400' }, '×') ) ) ), // Add custom service h('div', { className: 'flex gap-2 mb-6' }, h('input', { type: 'text', value: customService, onChange: e => setCustomService(e.target.value), onKeyPress: e => e.key === 'Enter' && addService(customService), className: 'input-field flex-1 px-4 py-3 rounded-lg text-white', placeholder: 'Add custom service...' }), h('button', { onClick: () => addService(customService), className: 'px-6 py-3 bg-emerald-500/20 text-emerald-400 rounded-lg font-medium hover:bg-emerald-500/30' }, 'Add') ), // Suggested services h('div', null, h('p', { className: 'text-sm text-gray-500 mb-3' }, 'Suggested services:'), h('div', { className: 'flex flex-wrap gap-2' }, SERVICE_TYPES[formData.serviceType].services .filter(s => !formData.services.includes(s)) .map(service => h('button', { key: service, onClick: () => addService(service), className: 'px-3 py-1 bg-gray-800 text-gray-400 rounded-lg text-sm hover:bg-gray-700 hover:text-white transition-all' }, `+ ${service}`) ) ) ), h('div', { className: 'flex gap-4 mt-8' }, h('button', { onClick: () => setStep(1), className: 'flex-1 py-4 rounded-xl font-bold text-gray-400 border border-gray-700 hover:border-gray-600' }, '← Back'), h('button', { onClick: () => setStep(3), disabled: !canProceed(), className: 'btn-primary flex-1 py-4 rounded-xl font-bold text-white' }, 'Continue to Areas →') ) ), // Step 3: Areas step === 3 && h('div', { className: 'card rounded-2xl p-8 glow-border fade-in' }, h('h2', { className: 'text-2xl font-bold text-white mb-2' }, 'Areas Covered'), h('p', { className: 'text-gray-400 mb-6' }, formData.areas.length > 0 ? `We found ${formData.areas.length} areas near your postcode. Remove any you don't cover, or add more.` : 'Add the towns/areas you service (minimum 3)'), // Selected areas formData.areas.length > 0 && h('div', { className: 'flex flex-wrap gap-2 mb-6' }, formData.areas.map(area => h('span', { key: area, className: 'tag px-3 py-2 rounded-lg text-white flex items-center gap-2' }, area, h('button', { onClick: () => removeArea(area), className: 'text-gray-400 hover:text-red-400' }, '×') ) ) ), // Add area h('div', { className: 'flex gap-2 mb-6' }, h('input', { type: 'text', value: customArea, onChange: e => setCustomArea(e.target.value), onKeyPress: e => e.key === 'Enter' && addArea(customArea), className: 'input-field flex-1 px-4 py-3 rounded-lg text-white', placeholder: 'Add town or area...' }), h('button', { onClick: () => addArea(customArea), className: 'px-6 py-3 bg-emerald-500/20 text-emerald-400 rounded-lg font-medium hover:bg-emerald-500/30' }, 'Add') ), h('p', { className: 'text-sm text-gray-500 mb-6' }, 'Tip: Add surrounding towns to create location pages that will help you rank for "[service] in [town]" searches.' ), h('div', { className: 'flex gap-4 mt-8' }, h('button', { onClick: () => setStep(2), className: 'flex-1 py-4 rounded-xl font-bold text-gray-400 border border-gray-700 hover:border-gray-600' }, '← Back'), h('button', { onClick: generateSite, disabled: !canProceed() || generating, className: 'btn-primary flex-1 py-4 rounded-xl font-bold text-white' }, generating ? `Generating... ${progress}%` : '🚀 Generate Website') ), generating && h('div', { className: 'mt-6 bg-gray-800 rounded-full h-2 overflow-hidden' }, h('div', { className: 'progress-bar h-full transition-all duration-300', style: { width: `${progress}%` } }) ) ), // Step 4: Complete step === 4 && h('div', { className: 'card rounded-2xl p-8 glow-border fade-in text-center' }, h('div', { className: 'text-6xl mb-6' }, '🎉'), h('h2', { className: 'text-2xl font-bold text-white mb-2' }, 'Website Generated!'), h('p', { className: 'text-gray-400 mb-8' }, 'Your website files have been downloaded. Here\'s what to do next:'), h('div', { className: 'text-left space-y-4 mb-8' }, h('div', { className: 'bg-gray-800/50 p-4 rounded-lg' }, h('h3', { className: 'font-bold text-white mb-2' }, '1. Extract the ZIP file'), h('p', { className: 'text-gray-400 text-sm' }, 'Unzip the downloaded file to see all your website pages.') ), h('div', { className: 'bg-gray-800/50 p-4 rounded-lg' }, h('h3', { className: 'font-bold text-white mb-2' }, '2. Deploy to Cloudflare Pages'), h('p', { className: 'text-gray-400 text-sm' }, 'Go to Cloudflare Pages → Create Project → Upload Assets → Drag your folder in.') ), h('div', { className: 'bg-gray-800/50 p-4 rounded-lg' }, h('h3', { className: 'font-bold text-white mb-2' }, '3. Connect your domain'), h('p', { className: 'text-gray-400 text-sm' }, 'Add a custom domain in Cloudflare Pages settings.') ), h('div', { className: 'bg-gray-800/50 p-4 rounded-lg' }, h('h3', { className: 'font-bold text-white mb-2' }, '4. Set up contact form (optional)'), h('p', { className: 'text-gray-400 text-sm' }, 'Create a free account at formspree.io and update the form action URL in contact.html.') ) ), h('div', { className: 'bg-emerald-500/10 border border-emerald-500/30 rounded-lg p-4 mb-8' }, h('p', { className: 'text-emerald-400 font-medium' }, `✨ ${4 + formData.services.length + formData.areas.length} pages generated`), h('p', { className: 'text-gray-400 text-sm mt-1' }, 'Home, Services, Areas, Contact + individual service & area pages') ), h('button', { onClick: () => { setStep(1); setFormData({ businessName: '', serviceType: 'drainage', location: '', phone: '', email: '', services: [], areas: [] }); }, className: 'btn-primary px-8 py-4 rounded-xl font-bold text-white' }, 'Generate Another Site') ) ), // Footer h('footer', { className: 'relative border-t border-white/10 mt-20' }, h('div', { className: 'max-w-5xl mx-auto px-6 py-8 text-center text-gray-500 text-sm' }, 'LocalSitePro — Generate professional service websites in minutes' ) ) ); } // Mount the app createRoot(document.getElementById('app')).render(h(App));